//
//  CADisplayLinkOnFrameViewController.m
//  CoreAnimation
//
//  Created by 张国文 on 16/10/31.
//  Copyright © 2016年 hangzhouyijiyin. All rights reserved.
//

#import "CADisplayLinkOnFrameViewController.h"

@interface CADisplayLinkOnFrameViewController ()

@property (nonatomic, strong) UIView *backgroundView;

@property (nonatomic, strong) UIImageView *ballImageV;

@property (nonatomic, strong) CADisplayLink *timer;;

@property (nonatomic, assign) CFTimeInterval duration;

@property (nonatomic, assign) CFTimeInterval timeOffset;

@property (nonatomic, assign) CFTimeInterval lastStep;

@property (nonatomic, strong) id fromValue;

@property (nonatomic, strong) id toValue;

@property (nonatomic, assign) NSInteger retainProgressCount; // 记录计时器的次数

@end

@implementation CADisplayLinkOnFrameViewController

/**
 注意到当创建CADisplayLink的时候，我们需要指定一个run loop和run loop mode，对于run loop来说，我们就使用了主线程的run loop，因为任何用户界面的更新都需要在主线程执行，但是模式的选择就并不那么清楚了，每个添加到run loop的任务都有一个指定了优先级的模式，为了保证用户界面保持平滑，iOS会提供和用户界面相关任务的优先级，而且当UI很活跃的时候的确会暂停一些别的任务。
 一个典型的例子就是当是用UIScrollview滑动的时候，重绘滚动视图的内容会比别的任务优先级更高，所以标准的NSTimer和网络请求就不会启动。
 
 RunLoop的模式：
1、 NSDefaultRunLoopMode - 标准优先级
 2、NSRunLoopCommonModes - 高优先级
 3、UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画
 
 在我们的例子中，我们是用了NSDefaultRunLoopMode，但是不能保证动画平滑的运行，所以就可以用NSRunLoopCommonModes来替代。但是要小心，因为如果动画在一个高帧率情况下运行，你会发现一些别的类似于定时器的任务或者类似于滑动的其他iOS动画会暂停，直到动画结束。
 
⚠️结论：我将CADisplayLink加入到不同模式的runLoop中，比如12、23、13、1、2进行组合，经过测试发现CADisplayLink的回调方法执行的次数和组合种类没有关系，只和cpu的使用率有关。比如：我将for循环执行1000次，那么回调方法会走80次，for循环执行2000次，回调方法走100次，for循环不执行的情况下，回调方法走了120次。当然这个回调方法会和电脑的配置有关系，我这个只是一个实验，通过这个实验已经可以确定当CPU比较繁忙的情况下CADisplayLink的执行次数(屏幕刷新的频率和回调方法的次数是相同的)并不总是1秒60次，而是会大于这个数值。
 
 */

/**
 *  CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类，它总是在屏幕完成一次更新之前启动，它的接口设计的和NSTimer很类似，所以它实际上就是一个内置实现的替代，但是和timeInterval以秒为单位不同，CADisplayLink有一个整型的frameInterval属性，指定了间隔多少帧之后才执行。默认值是1，意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒，你可以指定frameInterval为2，就是说动画每隔一帧执行一次（一秒钟30帧）或者3，也就是一秒钟20次，等等。
 
 ⚠️⚠️⚠️用CADisplayLink而不是NSTimer，会保证帧率足够连续，使得动画看起来更加平滑，但即使CADisplayLink也不能保证每一帧都按计划执行，一些失去控制的离散的任务或者事件（例如资源紧张的后台程序）可能会导致动画偶尔地丢帧。当使用NSTimer的时候，一旦有机会计时器就会开启(2秒的动画不一定会2秒执行完，通常在占用内存比较大的时候会大于2秒)，但是CADisplayLink却不一样：如果它丢失了帧，就会直接忽略它们，然后在下一次更新的时候接着运行(意思就是说你设定2秒的动画，那么就一定是2秒执行完，在内存占用比较大即使中间有丢帧也会严格在2秒的时候执行完毕)。因此使用CADisplayLink的时候每次执行回调的时间并不总是1 / 60.0，这个次数是根据系统cpu的负担适量的变小的(内存越紧张执行次数越少(CADisplayLink的调用次数适合屏幕的刷新频率相同的))，因此我们不能使用固定1 / 60.0加上timeOffset得到相应的timeOffset，只能根据CACurrentMediaTime获取屏幕刷新的时间获取差值进行计算。
 
 无论是使用NSTimer还是CADisplayLink，我们仍然需要处理一帧的时间超出了预期的六十分之一秒。由于我们不能够计算出一帧真实的持续时间，所以需要手动测量。我们可以在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间，然后和上一帧记录的时间去比较。
 */

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [self.timer invalidate];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    self.backgroundView = [[UIView alloc] initWithFrame:self.view.frame];
    self.backgroundView.backgroundColor = [UIColor colorWithRed:150 / 255.0 green:150 / 255.0 blue:150 / 255.0 alpha:1];
    [self.view addSubview:_backgroundView];
    
    self.ballImageV = [[UIImageView alloc] initWithFrame:CGRectMake(150, 50, 64, 64)]; // 此时center 182,82
    self.ballImageV.image = [UIImage imageWithName:@"ball"];
    [self.backgroundView addSubview:_ballImageV];
    
    
    
    UIButton *startButton = [UIButton buttonWithType:UIButtonTypeCustom];
    startButton.frame = CGRectMake(0, 450, 375, 30);
    startButton.backgroundColor = [UIColor whiteColor];
    [startButton setTitle:@"开始小球掉落动画" forState:UIControlStateNormal];
    [startButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [startButton addTarget:self action:@selector(start:) forControlEvents:UIControlEventTouchUpInside];
    startButton.tag = 100;
    [self.backgroundView addSubview:startButton];
}

- (void)start:(UIButton *)button
{
    self.fromValue = [NSValue valueWithCGPoint:CGPointMake(182, 82)];
    self.toValue = [NSValue valueWithCGPoint:CGPointMake(182, 400)];
    self.duration = 2.0;
    self.timeOffset = 0.0;
    [self.timer invalidate]; // 这个方法会在timer存在的时候移除timer，不存在的时候什么也不做
    self.retainProgressCount = 0;
    self.lastStep = CACurrentMediaTime(); // 获取当前帧开始刷新的时候的时间
    self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(goTime:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
   // [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode]; // 测试时候用的，可以保证UIScrollView滑动的时候动画仍然可以执行
    // 这个是测试CADisplayLink的工作方式，你会发现会在输出完1~1000个数字之后，计时器才会运行
    for (int i = 0; i < 1000; i++) {
        NSLog(@"%d", i);
    } // ok，加了这个for循环之后你会发现gotime的次数并不是120次，而是要比120次少，因此这个说明屏幕刷新之前执行的这个方法并不总是1 / 60.0，这个次数是根据系统cpu的负担适量的变小的(内存越紧张执行次数越少)，因此我们不能使用固定1 / 60.0加上timeOffset得到相应的timeOffset，只能根据CACurrentMediaTime获取屏幕刷新的时间获取差值进行计算。
    
    
}

- (void)goTime:(CADisplayLink *)timer
{
    NSLog(@"开始");
   
    CFTimeInterval thisStep = CACurrentMediaTime();
    CFTimeInterval stepDuration = thisStep - self.lastStep;
    NSLog(@"stepDuration = %f", stepDuration);
    self.lastStep = thisStep;
    self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration); // 最大到self.duration
  //  NSLog(@"timeOffset = %f", self.timeOffset);
    float time = self.timeOffset / self.duration;
    time = bounceEaseOut2(time);
    id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time];
    self.ballImageV.layer.position = [position CGPointValue];
    if (self.timeOffset == self.duration) {
      
        [self.timer invalidate]; // 当self.timer存在于runLoop中的时候，从runLoop当中释放，并且是self.timer的引用计数减一
         self.timer = nil; // 释放内存，因为每次开始动画的时候都会创建一个新的self.timer，这样不会造成内存的浪费
//        if (self.timer) {
//            NSLog(@"执行");
//        } else {
//            NSLog(@"不执行");
//        }
        NSLog(@"次数=%ld", self.retainProgressCount);
    }
     self.retainProgressCount++;
}


// 获取对应时间的值
float interpolate2(float from, float to, float time)
{
    return (to - from) * time + from;
}

- (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
{
    // 判断类型
    if ([fromValue isKindOfClass:[NSValue class]]) {
        // 获取具体类型
        const char *type = [fromValue objCType];
        // NSLog(@"类型：%s", type);
        // @encode将类型编码成为objCType
        if (strcmp(type, @encode(CGPoint)) == 0) {
            CGPoint from = [fromValue CGPointValue];
            CGPoint to = [toValue CGPointValue];
            
            CGPoint result = CGPointMake(interpolate2(from.x, to.x, time), interpolate2(from.y, to.y, time));
            if (result.y > 400) {
                
            } else {
                return [NSValue valueWithCGPoint:result];
            }
            
        }
    }
    // 如果不是NSValue类型的，返回一个安全值
    return (time < 0.5)? fromValue :toValue;
    
}

// 这可以起到作用，但效果并不是很好，到目前为止我们所完成的只是一个非常复杂的方式来使用线性缓冲复制CABasicAnimation的行为。这种方式的好处在于我们可以更加精确地控制缓冲，这也意味着我们可以应用一个完全定制的缓冲函数。那么该如何做呢？缓冲背后的数学并不很简单，但是幸运的是我们不需要一一实现它。罗伯特·彭纳有一个网页关于缓冲函数（http://www.robertpenner.com/easing），包含了大多数普遍的缓冲函数的多种编程语言的实现的链接，包括C。

float bounceEaseOut2(float t)
{
    if (t < 4/11.0) {
        return (121 * t * t)/16.0;
    } else if (t < 8/11.0) {
        return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
    } else if (t < 9/10.0) {
        return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
    }
    return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
}

@end
